function TextModel(){
    this.$initEventable();
    
    this.m_chunkedText = new ChunkedText_();
    this.m_lineDelimiters = [];
    this.m_lineOffsets = [0];
    this.m_tabStop = 4;
}

var _doubleStopCharacterMap = { 0x3000: 1 };
var _ranges = [
    [0x1100,0x115F], [0x11A3,0x11A7], [0x11FA,0x11FF], [0x2329,0x232A],
    [0x2E80,0x2E99], [0x2E9B,0x2EF3], [0x2F00,0x2FD5], [0x2FF0,0x2FFB],
    [0x3000,0x303E], [0x3041,0x3096], [0x3099,0x30FF], [0x3105,0x312D],
    [0x3131,0x318E], [0x3190,0x31BA], [0x31C0,0x31E3], [0x31F0,0x321E],
    [0x3220,0x3247], [0x3250,0x32FE], [0x3300,0x4DBF], [0x4E00,0xA48C],
    [0xA490,0xA4C6], [0xA960,0xA97C], [0xAC00,0xD7A3], [0xD7B0,0xD7C6],
    [0xD7CB,0xD7FB], [0xF900,0xFAFF], [0xFE10,0xFE19], [0xFE30,0xFE52],
    [0xFE54,0xFE66], [0xFE68,0xFE6B], [0xFF01,0xFF60], [0xFFE0,0xFFE6]
];
for (var _i=0; _i<_ranges.length; _i++) {
    var _range = _ranges[_i];
    for (var _j=_range[0]; _j<_range[1]; _j++) {
        _doubleStopCharacterMap[String.fromCharCode(_j)] = 1;
    }
}

var _lineDelimiterRegex = /\r?\n/g;

TextModel.prototype = {
    tabStop: function (){
        return this.m_tabStop;
    },
    setTabStop: function (stop){
        this.m_tabStop = 4;
    },
    getText: function (){
        return this.m_chunkedText.getText();
    },
    setText: function (text){
        this.replaceText(0, this.getTextLength(), text);
        return text;
    },
    getTextLength: function (){
        return this.m_lineOffsets[this.m_lineOffsets.length - 1];
    },
    replaceText: function (offset, length, text){
        var oldText = this.m_chunkedText.getSubstring(offset, length);
        this.m_chunkedText.replaceText(offset, length, text);
        this._updateLines(offset, length, text);
        this.dispatchEvent({
            type: 'textchanged',
            offset: offset,
            oldText: oldText,
            newText: text
        });
        return offset + text.length;
    },
    _updateLines: function (offset, length, text){
        // todo: 这里需要注释和改进.
        var offsets = [];
        var delimiters = [];
        var m;
        _lineDelimiterRegex.lastIndex = 0;
        while (m = _lineDelimiterRegex.exec(text)) {
            offsets.push(offset + _lineDelimiterRegex.lastIndex);
            delimiters.push(m[0]);
        }
        var index = this.getLineIndex(offset);
        var endIndex = this.getLineIndex(offset + length);
        var array = [index,  endIndex - index];
        array.push.apply(array, delimiters);
        array.splice.apply(this.m_lineDelimiters, array);
        array = [index, endIndex - index];
        array.push.apply(array, offsets);
        array.splice.apply(this.m_lineOffsets, array);
        var newIndex = index + offsets.length;
        var delta = text.length - length;
        var k = this.m_lineOffsets.length;
        while (k -- > newIndex) {
            this.m_lineOffsets[k] += delta;
        }
        this.dispatchEvent({
            type: 'lineschanged',
            lineIndex: index,
            endLineIndex: endIndex + 1,
            newEndLineIndex: newIndex + 1
        });
    },
    getTextSlice: function (start, end){
        end = end >= 0 ? end : this.getTextLength() + (end | 0);
        return this.m_chunkedText.getSubstring(start, end - start);
    },
    getLineDelimiter: function (index){
        return this.m_lineDelimiters[index] || '';
    },
    getLineIndex: function (offset){
        var offsets = this.m_lineOffsets;
        var start = 0;
        var end = offsets.length - 1;
        while (start < end) {
            var middle = (start + end) >> 1;
            if (offsets[middle] <= offset) {
                start = middle + 1;
            } else if (offsets[middle + 1] > offset) {
                end = middle;
            } else {
                return middle;
            }
        }
        return start;
    },
    getLineCount: function (){
        return !this.getTextLength() ? 0 : this.m_lineOffsets.length;
    },
    getLineStartOffset: function (index){
        return index == 0 ? 0 : this.m_lineOffsets[index - 1];
    },
    getLineEndOffset: function (index){
        return this.m_lineOffsets[index] - this.getLineDelimiter(index).length;
    },
    getLineText: function (index){
        return this.getTextSlice(this.getLineStartOffset(index), this.getLineEndOffset(index));
    },
    getLineEndStop: function (index){
        return this.getStopByInlineOffset(index,
            this.getLineEndOffset(index) - this.getLineStartOffset(index));
    },
    getInlineOffsetAndStopByStop: function (index, stop){
        var tabStop = this.m_tabStop;
        var text = this.getLineText(index);
        var offset = 0;
        var iStop = 0;
        while (iStop < stop && offset < text.length) {
            var ch = text.charAt(offset);
            if (ch == '\t') {
                iStop += tabStop - iStop % tabStop;
            } else if (_doubleStopCharacterMap[ch]) {
                iStop += 2;
            } else {
                iStop ++;
            }
            offset ++;
        }
        return [offset, iStop];
    },
    getStopByInlineOffset: function (index, offset){
        if (offset == 0) return 0;
        var tabStop = this.m_tabStop;
        var text = this.getLineText(index);
        if (offset > text.length) {
            offset = text.length;
        }
        var iStop = 0;
        for (var i=0; i<offset; i++) {
            var ch = text.charAt(i);
            if (ch == '\t') {
                iStop += tabStop - iStop % tabStop;
            } else if (_doubleStopCharacterMap[ch]) {
                iStop += 2;
            } else {
                iStop ++;
            }
        }
        return iStop;
    }
};

Utils.extend(TextModel, Eventable);